/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import { ts, ArkFile } from "arkanalyzer/lib";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { Defects, IssueReport } from '../../model/Defects';
import { MatcherCallback, MatcherTypes } from "../../matcher/Matchers";
import { AstMatcher } from '../../Index';
import { Utils } from '../../Index';
import { Rule } from "../../model/Rule";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { RuleFix } from '../../model/Fix';

type Options = {
    allowSingleExtends?: boolean;
};

interface LineColumnInfo {
    line: number,
    character: number
}

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoEmptyInterfaceCheck');
const gmetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/no-empty-interface.md", // TODO: support url
    description: "Disallow the declaration of empty interfaces."
};

export class NoEmptyInterfaceCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gmetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private emptyInterfaces: LineColumnInfo[] = [];
   private defaultOptions: Options = {
        allowSingleExtends: false,
    };
    private options: Options = { ...this.defaultOptions };
    private issueMap: Map<string, IssueReport> = new Map();

    private buildMatcher: AstMatcher = {
        matcherType: MatcherTypes.AST,
        match(node: ts.Node): boolean {
            return ts.isInterfaceDeclaration(node);
        }
    };

    public registerMatchers(): MatcherCallback[] {
        const matchBuildCb: MatcherCallback = {
            matcher: this.buildMatcher,
            callback: this.check
        };
        return [matchBuildCb];
    };


    public check = (node: ts.InterfaceDeclaration, arkFile: ArkFile): void => {
        this.parseOptions();
        if (this.isEmptyInterface(node)) {
            this.handleEmptyInterface(node, arkFile, node.getSourceFile());
        }
        this.reportSortedIssues();
    };

    private parseOptions(): void {
        if (this.rule?.option?.[0]) {
            const ruleOption = this.rule.option[0] as Options;
            this.options = {
                allowSingleExtends: ruleOption.allowSingleExtends ?? this.defaultOptions.allowSingleExtends
            };
        };
    };

    private isEmptyInterface(node: ts.InterfaceDeclaration): boolean {
        if (node.members.length > 0) {
            return false;
        }
        const baseTypes = node.heritageClauses?.map(clause => clause.types).flat() || [];
        if (baseTypes.length === 1) {
            return !this.options.allowSingleExtends;
        } else if (baseTypes.length > 1) {
            return false;
        } else {
            return true;
        }
    }
    
    private handleEmptyInterface(node: ts.InterfaceDeclaration, file: ArkFile, sourceFile: ts.SourceFile): void {
        if (node.heritageClauses) {
            this.handleEmptyInterfaceWithHeritage(node, file, sourceFile);
        } else {
            this.handleEmptyInterfaceWithoutHeritage(node, file, sourceFile);
        }
    }

    private handleEmptyInterfaceWithHeritage(node: ts.InterfaceDeclaration, file: ArkFile, sourceFile: ts.SourceFile): void {
        this.metaData.description = 'An interface declaring no members is equivalent to its supertype.';
        const name = node.name;
        const fixText = this.generateFixText(node, node.getText(), name.getText());
        const position = ts.getLineAndCharacterOfPosition(sourceFile, name.getStart());
        const line = position.line + 1;
        const character = position.character + 1;
        this.emptyInterfaces.push({ line, character });
        const defect = this.addIssueReport(file, { line, character });
        if (fixText && defect) {
            const fix: RuleFix = this.ruleFix(node.getStart(), node.getEnd(), fixText);
            this.issueMap.set(defect.fixKey, { defect, fix });
        }
    }

    private handleEmptyInterfaceWithoutHeritage(node: ts.InterfaceDeclaration, file: ArkFile, sourceFile: ts.SourceFile, ): void {
        const position = this.getAstPosition(node.name, sourceFile);
        this.metaData.description = 'An empty interface is equivalent to `{}`.';
        this.emptyInterfaces.push(position);
        const defect = this.addIssueReport(file, position);
        if (defect) {
            defect.fixable = false;
            this.issueMap.set(defect.fixKey, { defect, fix: undefined });
        }
    }

    private reportSortedIssues(): void {
        if (this.issueMap.size === 0) {
            return;
        }
        const sortedIssues = Array.from(this.issueMap.entries())
            .sort(([keyA], [keyB]) => Utils.sortByLineAndColumn(keyA, keyB));
        this.issues = [];
        sortedIssues.forEach(([_, issue]) => {
            RuleListUtil.push(issue.defect);
            this.issues.push(issue);
        });
    }

    private getAstPosition(node: ts.Node, sourceFile: ts.SourceFile): LineColumnInfo {
        const start = node.getStart();
        const position = ts.getLineAndCharacterOfPosition(sourceFile, start);
        return {
            line: position.line + 1,
            character: position.character + 1
        };
    }
    
    private generateFixText(node: ts.InterfaceDeclaration, nodeText: string, text: string): string | undefined {
        const baseTypeText = node.heritageClauses?.map(clause => clause.types).flat()[0].getText();
        const genericRealTypesText = node.typeParameters?.map(param => param.name.text).join(', ') || '';
        return `${nodeText.startsWith('export') ? 'export ' : ''}type ${text}${genericRealTypesText ? `<${genericRealTypesText}>` : ''} = ${baseTypeText}`;
    }

    private ruleFix(pos: number, end: number, fixText: string,): RuleFix {
        return { range: [pos, end], text: fixText };
    }

    private addIssueReport(arkFile: ArkFile, lineAndColumn: LineColumnInfo): Defects | void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const warnInfo = this.getLineAndColumn(arkFile, lineAndColumn);
        if (warnInfo) {
            const filePath = arkFile.getFilePath();
            let defects = new Defects(warnInfo.line, warnInfo.startCol,
                warnInfo.endCol, this.metaData.description, severity, this.rule.ruleId, filePath,
                this.metaData.ruleDocPath, true, false, true);
            this.defects.push(defects);
            return defects;
        }
    }

    private getLineAndColumn(arkfile: ArkFile, lineColumn: LineColumnInfo): {
        line: number, startCol: number, endCol: number, filePath: string
    } | null {
        if (arkfile) {
            const originPath = arkfile.getFilePath();
            return {
                line: lineColumn.line,
                startCol: lineColumn.character,
                endCol: lineColumn.character,
                filePath: originPath
            };
        } else {
            logger.debug('arkFile is null');
        }
        return null;
    }
}